package com.inexas.cu.rebind;

import java.util.*;
import com.google.gwt.core.ext.*;
import com.google.gwt.core.ext.typeinfo.*;
import com.inexas.cu.client.mvp.*;

// todo Possible problems
// - Private inner classes
// - Private members (or members without getters)
// - Name clashes
// - Find out if I can access private members

public class BinderGenerator extends Generator {
	private DeferredBindingJavaSource o;

	@Override
	public String generate(TreeLogger logger, GeneratorContext context, String typeName)
	        throws UnableToCompleteException {

		try {
			final TypeOracle typeOracle = context.getTypeOracle();
			final JClassType classType = typeOracle.getType(typeName);

			// Output will be the same package as the PM
			final String packageName = classType.getPackage().getName();
			final String inputClassName = classType.getSimpleSourceName();
			final String outputClassName = inputClassName + "Bound";

			o = new DeferredBindingJavaSource( //
			        logger, //
			        context, //
			        packageName, //
			        outputClassName);

			if(!o.isAlreadyWritten()) {
				final VistingCard vistingCard = new VistingCard(typeOracle);
				visit(vistingCard, classType);
				final List<TupleInfo> tupleInfos = vistingCard.tupleInfos;
				final int tupleCount = tupleInfos.size();

				// Imports
				o.addImport("com.inexas.cu.client.mvp.*");

				// Extend the input class...
				o.setSuperclass(packageName + '.' + inputClassName);

				// Implements Binder
				o.addImplementedInterface(Binder.class);

				// Write the constructor...
				emptyLine();
				o.println("// Called to do the initial binding");
				o.println("public " + outputClassName + "() {");
				o.indent();
				o.println("bind(this);");
				o.outdent();
				o.println("}");
				emptyLine();

				// Write methods for adding and removing Tuples to TupleLists...
				// !todo Don't I need an unbind() too?
				o.println("// Called when adding new Tuples to TupleLists");
				o.println("public void bind(Observer parent, Tuple tuple) {");
				o.indent();
				o.println("bind(tuple);");
				o.println("tuple.addObserver(parent);");
				o.outdent();
				o.println("}");
				emptyLine();

				// Write the main 'switch on tuple type' method
				o.println("// Selector to choose which Tuple to bind");
				o.println("public void bind(Tuple tuple) {");
				o.indent();
				o.println("final Class<? extends Tuple> tupleClass = tuple.getClass();");
				o.println("if(tupleClass == " + outputClassName + ".class) {");
				o.indent();
				o.println("bind" + inputClassName + "((" + inputClassName + ")tuple);");
				o.outdent();
				for(int i = 1; i < tupleCount; i++) {
					final TupleInfo tupleInfo = tupleInfos.get(i);
					final String tupleName = tupleInfo.name;
					// todo Beware of inner classes
					o.println("} else if(tupleClass == " + tupleName + ".class) {");
					o.indent();
					o.println("bind" + tupleName + "((" + tupleName + ")tuple);");
					o.outdent();
				}
				o.println("} else {");
				o.indent();
				o.println("throw new RuntimeException(\"Class not handled: \" + tupleClass.getName());");
				o.outdent();
				o.println("}");
				o.outdent();
				o.println("}");
				emptyLine();

				// Write a binder method for each tuple...
				for(int i = 0; i < tupleCount; i++) {
					final TupleInfo tupleInfo = tupleInfos.get(i);
					final String tupleName = tupleInfo.name;

					// Start method...
					o.println("private void bind" + tupleName + "(" + tupleName + " tuple) {");
					o.indent();

					if(tupleInfo.propertyNames.size() > 0) {
						o.println("// Bind the Properties...");
						for(final String name : tupleInfo.propertyNames) {
							final String get = getGetterName(name);
							o.println("tuple." + get + "().addObserver(tuple);");
						}
						emptyLine();
					}

					if(tupleInfo.propertyListNames.size() > 0) {
						o.println("// Bind PropertyLists...");
						for(final String name : tupleInfo.propertyListNames) {
							final String get = getGetterName(name);
							o.println("tuple." + get + "().addObserver(tuple);");
						}
						emptyLine();
					}

					if(tupleInfo.tupleNames.size() > 0) {
						o.println("// Bind Tuples...");
						for(final String name : tupleInfo.tupleNames) {
							final String get = getGetterName(name);
							final String tupleTypeName = tupleInfo.classType.getQualifiedSourceName();
							o.println("final " + tupleTypeName + " childTuple = tuple." + get + "();");
							o.println("bind" + tupleTypeName + "(childTuple);");
							o.println("childTuple.addObserver(tuple);");
						}
						emptyLine();
					}

					if(tupleInfo.tupleListNames.size() > 0) {
						o.println("// Bind TupleLists...");
						for(final String name : tupleInfo.tupleListNames) {
							final String get = getGetterName(name);
							final String tupleTypeName = tupleInfo.classType.getQualifiedSourceName();
							o.println("final TupleList<" + tupleTypeName + "> tupleList = tuple." + get + "();");
							o.println("for(final " + tupleTypeName + " childTuple : tupleList) {");
							o.indent();
							o.println("bind" + tupleTypeName + "(childTuple);");
							o.println("childTuple.addObserver(tupleList);");
							o.outdent();
							o.println("}");
							o.println("tupleList.addObserver(tuple);");
							o.println("tupleList.setBinder(this);");
						}
					}

					// End of method
					o.outdent();
					o.println("}");
					emptyLine();
				}

				// Write an unbinder 'switch' method...

				// Start the switch with the PM tuple...
				o.println("public void unbind(Observer parent, Tuple tuple) {");
				o.indent();
				o.println("final Class<? extends Tuple> tupleClass = tuple.getClass();");
				o.println("if(tupleClass == " + inputClassName + ".class) {");
				o.indent();
				o.println("unbindTestPm(null, (" + inputClassName + ")tuple);");
				o.outdent();

				// Continue the switch for the child descendents...
				for(int i = 1; i < tupleCount; i++) {
					final TupleInfo tupleInfo = tupleInfos.get(i);
					final String tupleName = tupleInfo.name;
					o.println("} else if(tupleClass == " + tupleName + ".class) {");
					o.indent();
					o.println("unbind" + tupleName + "(parent, (" + tupleName + ")tuple);");
					o.outdent();
				}

				// Add a just-in-case else...
				o.println("} else {");
				o.indent();
				o.println("throw new RuntimeException(\"Class not handled: \" + tupleClass.getName());");
				o.outdent();
				o.println("}");

				// End the method...
				o.outdent();
				o.println("}");

				// Write an unbinder for each Tuple member of the tree...
				for(int i = 0; i < tupleCount; i++) {
					final TupleInfo tupleInfo = tupleInfos.get(i);
					final String tupleName = tupleInfo.name;

					// Start the method...
					o.println("private void unbind" + tupleName + "(Observer parent, " + tupleName + " tuple) {");
					o.indent();

					// Properties...
					for(final String name : tupleInfo.propertyNames) {
						final String get = getGetterName(name);
						o.println("tuple." + get + "().removeObserver(tuple);");
					}

					// PropertyLists...
					for(final String name : tupleInfo.propertyListNames) {
						final String get = getGetterName(name);
						o.println("tuple." + get + "().removeObserver(tuple);");
					}

					// Tuples...
					for(final String name : tupleInfo.tupleNames) {
						final String get = getGetterName(name);
						o.println("final " + name + " childTuple = tuple." + get + "();");
						o.println("unbind" + name + "(tuple, childTuple);");
					}

					// TupleLists...
					for(final String name : tupleInfo.tupleListNames) {
						final String get = getGetterName(name);
						o.println("final TupleList<" + name + "> tupleList = tuple." + get + "();");
						o.println("for(final " + name + " childTuple : tupleList) {");
						o.indent();
						o.println("unbindTuple(tupleList, childTuple);");
						o.println("childTuple.removeObserver(tupleList);");
						o.outdent();
						o.println("}");
						o.println("tupleList.removeObserver(tuple);");
						o.println("if(parent != null) {");
						o.indent();
						o.println("tuple.removeObserver(parent);");
						o.outdent();
						o.println("}");
					}

					// End the method...
					o.outdent();
					o.println("}");
				} // End of writing tuple unbinders

				// The class is started for us automagically, but not
				// terminated...
				o.endClass();

				// commit generated class
				o.commit(logger);
			}
		} catch(final Exception e) {
			logger.log(TreeLogger.ERROR, "Hello World Generator error", e);
		}

		return o.getFullyQualifiedName();
	}

	private class VistingCard {
		final List<TupleInfo> tupleInfos = new ArrayList<TupleInfo>();
		final JClassType propertyType;
		final JClassType propertyListType;
		final JClassType tupleType;
		final JClassType tupleListType;
		
		public VistingCard(TypeOracle typeOracle) {
			try {
	            propertyType = typeOracle.getType(Property.class.getName());
	            propertyListType = typeOracle.getType(PropertyList.class.getName());
	            tupleType = typeOracle.getType(Tuple.class.getName());
	            tupleListType = typeOracle.getType(TupleList.class.getName());
            } catch(final NotFoundException e) {
            	throw new RuntimeException("Error accessing types", e);
            }
        }

		public boolean containsTuple(JClassType classType) {
			for(final TupleInfo tupleInfo : tupleInfos) {
				if(tupleInfo.classType.equals(classType)) {
					return true;
				}
			}
			return false;
        }
	}
	
	private void visit(VistingCard vc, JClassType classType) {
        // We might already have processed this tuple
        if(!vc.containsTuple(classType)) {
        	final JField[] fields = classType.getFields();
        	final JMethod[] methods = classType.getMethods();
        	// Add the tuple info now so the first entry is the PM
        	final TupleInfo tupleInfo = new TupleInfo(classType);
        	vc.tupleInfos.add(tupleInfo);
        	
        	for(final JField field : fields) {
        		final JClassType type = (JClassType)field.getType();
        		if(type.isAssignableTo(vc.propertyType)) {
        			final String getter = getGetterName(field, methods);
        			tupleInfo.propertyNames.add(getter);
        		} else if(type.isAssignableTo(vc.propertyListType)) {
        			final String getter = getGetterName(field, methods);
        			tupleInfo.propertyListNames.add(getter);
        		} else if(type.isAssignableTo(vc.tupleType)) {
        			final String getter = getGetterName(field, methods);
        			tupleInfo.tupleNames.add(getter);
        			visit(vc, type);
        		} else if(type.isAssignableTo(vc.tupleListType)) {
        			final String getter = getGetterName(field, methods);
        			tupleInfo.tupleListNames.add(getter);
        			final JClassType childClassType =	//
        				((JParameterizedType)type).isParameterized().getTypeArgs()[0];
        			visit(vc, childClassType);
        		} else {
        			// Ignore other types
        		}
        	}
        }
	}

	private String getGetterName(JField field, JMethod[] methods) {
		final String result = field.getName();
		final String getterName = getGetterName(result);
		int i;
		for(i = 0; i < methods.length; i++) {
			final JMethod method = methods[i];
			if( method.getName().equals(getterName) &&
					((JClassType)method.getReturnType()).isAssignableFrom((JClassType)(field.getType())) ) {
				break;
			}
		}
		if(i == methods.length) {
			throw new RuntimeException("Missing getter method for field: " +	//
					field.getEnclosingType().getName() + '.' + result);
		}
		return result;
    }
	
	private String getGetterName(String name) {
		final StringBuilder sb = new StringBuilder();
		sb.append("get");
		sb.append(Character.toUpperCase(name.charAt(0)));
		sb.append(name.substring(1));
		return sb.toString();
	}
	
	private void emptyLine() {
		o.println();
	}
}
